"""
Denormalisation fields - easy, low-effort denormalisation of data.

Contains two fields:
    MirrorField - A 'read-only' field that updates itself to the value of
                  a field on the other side of a ForeignKey. Useful for
                  keeping often-accessed attributes accessible without JOINs.
    
    AggregateField - Caches aggregate stats, such as "the number of photos
                     this user has" or "maximum age of a user in this group".
    
    Both of these fields tie into Django's signal system and only re-write
    themselves when the value they are referring changes.
    
    Example usage:
    
    user_username = MirrorField("user", "username")
    (note that the first field is the foreignkey name, not the model name!)
    
    photo_count = AggregateField("Photo", "fk:user", "count")
    (This field is the current count of all Photo objects where the user
    field points to the current instance).
"""


from django.db.models.fields import Field, IntegerField
from django.db.models.signals import post_save, pre_save, class_prepared, \
    post_delete
from django.db.models.fields.related import add_lazy_relation

from django.dispatch import Signal

# Some custom signals
mirror_update = Signal(providing_args=["instance"])
mirror_save = Signal(providing_args=["instance"])

# Make post_save trigger the two above signals in order, so we only need to save once
def propagate_save(instance, created, sender, **kwargs):
    mirror_update.send(sender=sender, instance=instance)
    mirror_save.send(sender=sender, instance=instance)
post_save.connect(propagate_save)


class MirrorFieldBase(Field):
    
    """
    Base class for MirrorFields (the fields themselves must dynamically
    inherit from an arbitary field class at runtime, and so are created in
    the MirrorField function below).
    """
    
    def __init__(self, foreign_key, other_field, *args, **kwargs):
        "Constructor."
        
        # Store what we're referring to
        self.foreign_key = foreign_key
        
        # Store other field name - we'll resolve it soon
        self.other_field = other_field
        
        # Sub-init preparation (not actually done till "setup")
        self.args = args
        self.kwargs = kwargs
    
    
    def contribute_to_class(self, cls, *args, **kwds):
        "Called to tell field to do things with the model it is on"
        
        # Save our parent class so we can query it to get instances to save to
        self.model = cls
        
        # Save params for the later c_t_c call
        self.ctc_args = args
        self.ctc_kwds = kwds
        
        # When the class is ready, resolve the foreign key.
        class_prepared.connect(self.setup, sender=self.model)
        
        # Listen out for creates on our own model
        pre_save.connect(self.self_saved, sender=self.model)
    
    
    def setup(self, **kwds):
        "Called when the host class is fully prepared, to resolve the FK."
        
        # Resolve our foreign key name
        foreign_key = self.model._meta.get_field(self.foreign_key)
        
        # Now we know the other model
        self.other_model = foreign_key.rel.to
        self.other_field = self.other_model._meta.get_field(self.other_field)
        self.base_class = self.other_field.__class__
        
        # Connect save signal to nab save events and write ourselves
        mirror_update.connect(self.other_mirror_update, sender=self.other_model)
        mirror_save.connect(self.other_mirror_save, sender=self.other_model)
        
        # MEEP MEEP Dynamically re-parent class
        # TODO: Try to not do this.
        self.__class__.__bases__ = (self.other_field.__class__, MirrorFieldBase)
        
        # Now, initialise this field
        self.kwargs.update({
            "blank": True,
            "null": self.other_field.null,
            "max_length": self.other_field.max_length,
        })
        self.base_class.__init__(self, *self.args, **self.kwargs)
        self.base_class.contribute_to_class(self, self.model, *self.ctc_args, **self.ctc_kwds)
    
    
    def other_mirror_update(self, instance, **kwargs):
        "Called when the target model is saved, to propagate values across."
        
        # Work out what instances of our own model are related
        if not hasattr(instance, "_denorm_instances"):
            instance._denorm_instances = self.model.objects.filter(
                **{self.foreign_key: instance.id}
            )
        
        # For each instance, update it with the new value
        for di in instance._denorm_instances:
            setattr(di, self.attname, getattr(instance, self.other_field.attname))
    
    
    def other_mirror_save(self, instance, **kwargs):
        # If there are denorm instances on the instance, save em
        if hasattr(instance, "_denorm_instances"):
            for di in instance._denorm_instances:
                di.save()
            del instance._denorm_instances
    
    
    def self_saved(self, instance, **kwargs):
        "Fills out a newly-created instance's values."
        
        if not instance.id: # Only on initial creation
            # Get the other instance
            other_instance = getattr(instance, self.foreign_key)
            # Save its value into us
            setattr(instance, self.attname, getattr(other_instance, self.other_field.attname))



def MirrorField(foreign_key, field, *args, **kwds):
    
    """
    Constructor for MirrorFields. Due to the way these fields' inheritance
    tree is changed at runtime, we must make a new class for each instance
    of the field.
    """
    
    class MirrorField(MirrorFieldBase):
        """
        A field which will replicate the contents of a field on another model.
        Note that this class will be dynamically re-parented at runtime so
        it inherits from the correct field type. Yuk, I know.
        """
        pass
    
    return MirrorField(foreign_key, field, *args, **kwds)



class AggregateFieldBase(Field):
    
    """
    The base class for AggregateFields.
    For each AggregateField, a new subclass of this is created in the
    constructor function AggregateField below, that also inherits from the
    correct Field subclass.
    """
    
    def __init__(self, model, linktype, linkvalue, function, field_type, field_kwargs={}):
        
        # Save the variables
        self.model = model
        self.link = (linktype, linkvalue)
        self.function = function
        self.field_type = field_type
        
        kwargs = {
            "blank": True,
            "null": True,
        }
        kwargs.update(field_kwargs)
        self.base_class.__init__(self, **kwargs)
    
    
    def contribute_to_class(self, cls, *args, **kwds):
        """
        Called when the class is ready for some input - we use it
        to hook into its created signal, and to resolve the model if it's
        a string.
        """
        
        self.parent_model = cls
        
        if isinstance(self.model, (str, unicode)):
            add_lazy_relation(cls, None, self.model, self.resolved_model)
        else:
            self.add_model_listener()
        
        # Listen out for creates on our own model
        post_save.connect(self.self_saved, sender=self.parent_model)
        
        # Call up the inheritance tree
        self.base_class.contribute_to_class(self, cls, *args, **kwds)
    
    
    def resolved_model(self, data, model, cls):
        "Callback for add_lazy_relation call above."
        self.model = model
        self.add_model_listener()
    
    
    def add_model_listener(self):
        "Adds a listener for creates/updates on the target model"
        pre_save.connect(self.model_pre_save, sender=self.model)
        post_save.connect(self.model_post_save, sender=self.model)
        post_delete.connect(self.model_post_delete, sender=self.model)
    
    
    def update_with_id(self, id):
        """
        Update logic for the fk: link type, for a given ID.
        Called twice on saves (once for the old and new value of the FK)
        and once on creation of new instances.
        """
        if isinstance(id, self.parent_model):
            our_instance = id
        else:
            try:
                our_instance = self.parent_model.objects.get(id=id)
            except self.parent_model.DoesNotExist:
                return False
        
        # Calculate function over all other models with that FK on that ID
        value = self.function(self.model.objects.filter(**{
            self.link[1]: id,
        }))
        setattr(our_instance, self.attname, value)
        our_instance._denorm_skip = True
        our_instance.save()
        del our_instance._denorm_skip
    
    
    def update_all(self):
        """
        If we have an all: link type, this will update the value of our field
        on all instances of our host model.
        """
        # Run the agg function over all of the other models
        value = self.function(self.model.objects.all())
        # This means, for all instances of _our_ model, we need to update
        self.parent_model.objects.all().update(**{self.attname: value})
    
    
    def model_pre_save(self, instance, **kwds):
        """
        Called before the save is committed; cannot actually do any calculations
        (as they rely on queries, which rely on things being in the DB)
        but stores the current (i.e. 'old') value of the FK for post_save.
        """
        
        if hasattr(instance, "_denorm_skip"):
            return # Else we can recurse around ourselves
        
        if self.link[0] == "fk":
            # Save the old FK value for later, only if it's not new
            if instance.id:
                self.old_id = getattr(instance.__class__.objects.get(id=instance.id), self.link[1]+"_id")
            else:
                self.old_id = None
    
    
    def model_post_save(self, instance, **kwds):
        "An instance of the other model has been saved, so update the aggregate"
        
        if hasattr(instance, "_denorm_skip"):
            return # Else we can recurse around ourselves
        
        # What we do now depends on our link type
        if self.link[0] == "all":
            self.update_all()
        
        elif self.link[0] == "fk":
            
            # Find the new FK value
            new_id = getattr(instance, self.link[1]+"_id")
            # Update the new ID value
            self.update_with_id(new_id)
            # Update the old ID value
            if self.old_id:
                self.update_with_id(self.old_id)
    
    
    def model_post_delete(self, instance, **kwds):
        """
        Called after the object is deleted; we re-run the aggregates for its
        old ID/everything.
        """
        if self.link[0] == "all":
            self.update_all()
        
        elif self.link[0] == "fk":
            # Find the deleted FK value
            id = getattr(instance, self.link[1]+"_id")
            # Update the new ID value
            self.update_with_id(id)
    
    
    def self_saved(self, instance, created, **kwargs):
        "Adds a value when a new instance of the host model is created"
        
        if created: # Only on initial creation
            
            # For ALL links
            if self.link[0] == "all":
                # Run the agg function over all of the other models
                value = self.function(self.model.objects.all())
                setattr(instance, self.attname, value)
                instance.save()
            
            # For FK links, we know the ID, so fire off an update.
            elif self.link[0] == "fk":
                self.update_with_id(instance)


# Builtin functions, for use directly as strings in the AggregateField
_aggregate_functions = {
    "max": (
        lambda qs, col: max([getattr(x, col) for x in qs]),
        IntegerField,
    ),
    "min": (
        lambda qs, col: min([getattr(x, col) for x in qs]),
        IntegerField,
    ),
    "count": (
        lambda qs, col: qs.count(),
        IntegerField,
    ),
}


def AggregateField(model, function, field_type=None, column=None, **kwargs):
    """
    Creates a new AggregateField. This constructor function actually
    creates a unique AggregateFieldBase subclass per call,
    which dynamically inherits from the right field type, based on the function.
    """
    
    
    # Check they gave builtin function or function and type
    if not isinstance(function, (str, unicode)):
        if not field_type:
            raise ValueError("You must provide a field_type= argument to AggregateField if you define your own function.")
        elif not isinstance(field_type, Field):
            raise ValueError("The field_type argument to an AggregateField must be a Field subclass.")
    else:
        # It must be a builtin, so look it up
        try:
            underlying_function, field_type = _aggregate_functions[function]
        except KeyError:
            raise ValueError("'%s is not a built-in aggregate function." % function)
        # Curry the column argument into the function
        function = lambda qs: underlying_function(qs, column)
    
    # Try to detect the link type
    if "fk" in kwargs and "all" in kwargs:
        raise ValueError("Please only use fk= or all=, nor both.")
    elif "fk" in kwargs:
        linktype, linkvalue = "fk", kwargs['fk']
    elif "all" in kwargs:
        linktype, linkvalue = "all", None
    else:
        raise ValueError("Please specify fk= or all= for the relation to the model.")
    
    # Create the new subclass and go
    class AggregateField(AggregateFieldBase, field_type):
        base_class = field_type
    
    return AggregateField(model, linktype, linkvalue, function, field_type)
